﻿/*
Bf3Rcon.NET, provides a .NET implementation of BF3's RCON interface.
Copyright (C) 2011 agentwite, Timi, Unseen, AlCapwn

This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

You can contact us at http://bf3rcon.codeplex.com/.
*/

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net.Sockets;
using System.IO;
using System.ComponentModel;

namespace System.Net.Battlefield3
{
    /// <summary>
    /// Provideds a single-threaded implementation of the Battlefield 3 RCON protocol.
    /// </summary>
    /// <remarks>
    /// Unlike <see cref="RconClient"/>, this class uses only synchronous socket methods, which also means there are no events.<para />
    /// Since RCON events aren't supported, IPs are not available.<para />
    /// The socket-related methods of this library rethrow <see cref="SocketException">SocketExceptions</see> after disconnecting.<para />
    /// The only event included in this class is <see cref="Disconnected"/>.<para /><para />
    /// This class is designed to work well with ASP.NET applications, since it only operates on a single thread. Since
    /// only a single thread is used, there's also no timer for keeping the socket alive during no activity.
    /// </remarks>
    public class LightRconClient
        : RconClientBase, IDisposable
    {
        NetworkStream Stream { get { return Socket.GetStream(); } }
        List<byte> LeftoverData = new List<byte>();
        //Queue<Packet> PacketQueue = new Queue<Packet>(2); //stores extra packets. technically should not be needed
        int ClientSequence = 1;

        void SetupSocket()
        {
            Socket = new TcpClient()
            {
                ReceiveTimeout = SynchronousReadTimeout * 1000,
                SendTimeout = SynchronousReadTimeout * 1000
            };
        }

        #region Events
        /// <summary>
        /// Occurs when the <see cref="RconClient"/> has disconnected from the server.
        /// </summary>
        public event EventHandler<DisconnectedEventArgs> Disconnected;
        /// <summary>
        /// Raises the <see cref="Disconnected"/> event of the <see cref="RconClient"/>.
        /// </summary>
        /// <param name="e">A <see cref="DisconnectedEventArgs"/> that contains the event data.</param>
        protected virtual void OnDisconnected(DisconnectedEventArgs e)
        {
            InvokeEvent<DisconnectedEventArgs>(Disconnected, this, e);
        }

        //just in case more events are added
        static void InvokeEvent<TArgs>(EventHandler<TArgs> eventHandler, object sender, TArgs e) where TArgs : EventArgs
        {
            if (eventHandler == null) return;

            foreach (EventHandler<TArgs> ev in eventHandler.GetInvocationList())
            {
                ISynchronizeInvoke target = ev.Target as ISynchronizeInvoke;

                try
                {
                    if (target != null && target.InvokeRequired)
                        target.Invoke(ev, new object[] { sender, e });
                    else
                        ev(sender, e);
                }
                catch (AppDomainUnloadedException)
                {
                    eventHandler -= ev;
                }
            }
        }
        #endregion

        /// <summary>
        /// Connects the <see cref="LightRconClient"/> to the server.
        /// </summary>
        public override void Connect()
        {
            if (IsConnected) return;

            SetupSocket();

            try
            {
                Socket.Connect(Address, Port);
            }
            catch (SocketException)
            {
                Socket.Close();
                Socket = null;

                throw;
            }
        }

        /// <summary>
        /// Disconnects the <see cref="LightRconClient"/> from the server.
        /// </summary>
        public override void Disconnect()
        {
            Disconnect(string.Empty, SocketError.Success);
        }

        /// <summary>
        /// Disconnects the <see cref="RconClientBase"/> whenever a <see cref="SocketException"/> occurs.
        /// </summary>
        /// <param name="message">The message of the <see cref="SocketException"/>.</param>
        /// <param name="sockError">The <see cref="SocketError"/> or the <see cref="SocketException"/>.</param>
        protected internal override void Disconnect(string message, Sockets.SocketError sockError)
        {
            Socket.Close();
            Socket = null;

            OnDisconnected(new DisconnectedEventArgs(message, sockError));
        }

        /// <summary>
        /// Sends a <see cref="Packet"/> to the server.
        /// </summary>
        /// <param name="packet">The <see cref="Packet"/> that is being sent.</param>
        protected override void SendPacket(Packet packet)
        {
            if (!IsConnected) return;

            try
            {
                Stream.Write(packet.ToByteArray(), 0, packet.NumberOfBytes);
            }
            catch (SocketException ex)
            {
                Disconnect(ex.Message, ex.SocketErrorCode);
                throw;
            }
            
        }

        object SendRequestObject = new object(); //although this class isn't meant to be used with threads, do a lock anyway
        /// <summary>
        /// Sends a request for information that returns the response <see cref="Packet"/>.
        /// </summary>
        /// <param name="words">The words of the <see cref="Packet"/> being sent.</param>
        /// <returns>The <see cref="Packet"/> set in response to the request or null (Nothing in Visual Basic) if a timeout occurs.</returns>
        /// <remarks>
        /// A timeout will occur when <see cref="RconClientBase.SynchronousReadTimeout"/> elapses.
        /// </remarks>
        public override Packet SendRequest(params string[] words)
        {
            lock (SendRequestObject)
            {
                if (!IsConnected) return null;

                try
                {
                    //send the request
                    SendPacket(new Packet(false, false, ClientSequence++, words));

                    //wait for a full packet to be received
                    while (!Packet.ContainsCompletePacket(LeftoverData))
                    {
                        //receive data
                        byte[] buffer = new byte[BufferSize];

                        int bytesRead = Stream.Read(buffer, 0, BufferSize);

                        if (bytesRead == 0)
                        {
                            Disconnect();
                            return null;
                        }

                        for (int i = 0; i < bytesRead; i++)
                            LeftoverData.Add(buffer[i]);
                    }
                }
                catch (IOException)
                {
                    return null;
                }
                catch (SocketException ex)
                {
                    Disconnect(ex.Message, ex.SocketErrorCode);
                    throw;
                }
                
                //a complete packet should now be available
                return new Packet(new List<byte>(LeftoverData), LeftoverData);
            }
        }

        /// <summary>
        /// Sends a request that came from internal code.
        /// </summary>
        /// <param name="words">The words of the <see cref="Packet"/> being sent.</param>
        /// <returns>The <see cref="Packet"/> set in response to the request or null (Nothing in Visual Basic) if a timeout occurs.</returns>
        /// <remarks>This method is the same as <see cref="SendRequest"/>.</remarks>
        protected internal override Packet InternalSendRequest(params string[] words)
        {
            return SendRequest(words);
        }

        /// <summary>
        /// Gets the IP of a <see cref="Player"/> (by name).
        /// </summary>
        /// <param name="name">The name of the <see cref="Player"/>.</param>
        /// <returns>Null because <see cref="LightRconClient"/> is not capable of getting IPs.</returns>
        protected internal override string GetPlayerIP(string name)
        {
            return null; //IPs aren't possible in this class because no PB event
        }

        #region IDisposable Members
        private bool disposed = false;

        /// <summary>
        /// Disposes the RconClient object.
        /// </summary>
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        /// <summary>
        /// Disposes the RconClient object.
        /// </summary>
        /// <param name="disposing">Whether or not to release managed resources.</param>
        protected virtual void Dispose(bool disposing)
        {
            if (!disposed)
            {
                if (disposing)
                {
                    Socket.Close();
                    Socket = null;
                }
            }

            disposed = true;
        }

        #endregion

    }
}
